package com.feiwo.nonego.rpc.spi.loader;

import com.feiwo.nonego.rpc.common.util.StringUtils;
import com.feiwo.nonego.rpc.spi.annotation.SPI;
import com.feiwo.nonego.rpc.spi.annotation.SPIClass;
import com.feiwo.nonego.rpc.spi.factory.ExtensionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 *
 *
 * @author feiwo 2025-08-12
 */
public class ExtensionLoader<T> {

    private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);

    private static final String SERVICE_DIRECTORY = "META-INF/services/";
    private static final String NONEGO_DIRECTORY = "META-INF/nonego/";
    private static final String NONEGO_DIRECTORY_EXTERNAL = "META-INF/nonego/external/";
    private static final String NONEGO_DIRECTORY_INTERNAL = "META-INF/nonego/internal/";

    private static final String[] SPI_DIRECTORIES = new String[]{
            SERVICE_DIRECTORY,
            NONEGO_DIRECTORY,
            NONEGO_DIRECTORY_EXTERNAL,
            NONEGO_DIRECTORY_INTERNAL
    };

    private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();

    private final Holder<Map<String, Class<?>>> cacheClasses = new Holder<>();
    private final Map<String, Holder<Object>> cacheInstances = new ConcurrentHashMap<>();
    private final Map<Class<?>, Object> spiClassInstance = new ConcurrentHashMap<>();

    private String cacheDefaultName;
    private final Class<T> clazz;
    private final ClassLoader classLoader;

    public ExtensionLoader(Class<T> clazz, ClassLoader classLoader) {
        this.clazz = clazz;
        this.classLoader = classLoader;
        if (!Objects.equals(clazz, ExtensionFactory.class)) {
            ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getSpiClassInstances();
        }
    }

    public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz, final ClassLoader classLoader) {
        Objects.requireNonNull(clazz,"extension clazz is null");
        if (!clazz.isInterface()) {
            throw new IllegalArgumentException("extension clazz ("+clazz+") is not interface");
        }
        if (!clazz.isAnnotationPresent(SPI.class)) {
            throw new IllegalArgumentException("extension clazz ("+clazz+") without @"+SPI.class+" annotation");
        }
        var extensionLoader =  LOADERS.get(clazz);
        if (Objects.nonNull(extensionLoader)) {
            //noinspection unchecked
            return (ExtensionLoader<T>) extensionLoader;
        }
        LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz,classLoader));
        //noinspection unchecked
        return (ExtensionLoader<T>) LOADERS.get(clazz);
    }

    public static <T> T getExtension(final Class<T> clazz, String name) {
        return StringUtils.isEmpty(name) ? getExtensionLoader(clazz).getDefaultSpiClassInstance() : getExtensionLoader(clazz).getSpiClassInstance(name);
    }

    public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {
        return getExtensionLoader(clazz, ExtensionLoader.class.getClassLoader());
    }

    public T getDefaultSpiClassInstance() {
        getExtensionClasses();
        if (StringUtils.isEmpty(cacheDefaultName)) {
            return null;
        }
        return getSpiClassInstance(cacheDefaultName);
    }

    public T getSpiClassInstance(final String name) {
        if (StringUtils.isEmpty(name)) {
            throw new NullPointerException("get spi class name is null");
        }
        var objHolder = cacheInstances.get(name);
        if (Objects.isNull(objHolder)) {
            cacheInstances.putIfAbsent(name, new Holder<>());
            objHolder = cacheInstances.get(name);
        }
        var val = objHolder.getVal();
        if (Objects.isNull(val)) {
            synchronized (cacheInstances) {
                val = objHolder.getVal();
                if (Objects.isNull(val)) {
                    val = createExtension(name);
                    objHolder.setVal(val);
                }
            }
        }
        //noinspection unchecked
        return (T)val;
    }

    public List<T> getSpiClassInstances() {
        var extensionClasses = getExtensionClasses();
        if (extensionClasses.isEmpty()) {
            return Collections.emptyList();
        }
        if (Objects.equals(extensionClasses.size(), cacheInstances.size())) {
            //noinspection unchecked
            return (List<T>) this.cacheInstances.values().stream().map(Holder::getVal).toList();
        }
        var instances = new ArrayList<T>();
        extensionClasses.forEach((name,val ) -> {
            var instance = this.getSpiClassInstance(name);
            instances.add(instance);
        });
        return instances;
    }

    private T createExtension(final String name) {
        var aClass = getExtensionClasses().get(name);
        if (Objects.isNull(aClass)) {
            throw new IllegalArgumentException("name is error");
        }
        var obj = spiClassInstance.get(aClass);
        if (Objects.isNull(obj)) {
            try {
                spiClassInstance.putIfAbsent(aClass, aClass.getDeclaredConstructor().newInstance());
                obj = spiClassInstance.get(aClass);
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException |
                     NoSuchMethodException e) {
                throw new IllegalStateException("extension instance (name: "+ name +", class: "+ aClass+") could not be instantiated: "+ e.getMessage(), e);
            }
        }
        //noinspection ReassignedVariable,unchecked
        return (T)obj;
    }

    public Map<String,Class<?>> getExtensionClasses() {
        var classes = cacheClasses.getVal();
        if (Objects.isNull(classes)) {
            synchronized (cacheClasses) {
                classes = cacheClasses.getVal();
                if (Objects.isNull(classes)) {
                    classes = loadExtensionClass();
                    cacheClasses.setVal(classes);
                }
            }
        }
        return classes;
    }

    private Map<String, Class<?>> loadExtensionClass() {
        var annotation = clazz.getAnnotation(SPI.class);
        if (Objects.nonNull(annotation)) {
            var val = annotation.value();
            if (!StringUtils.isEmpty(val)) {
                cacheDefaultName = val;
            }
        }
        var classes = new HashMap<String, Class<?>>(16);
        loadDirectory(classes);
        return classes;
    }

    private void loadDirectory(final HashMap<String, Class<?>> classes) {
        for (String directory : SPI_DIRECTORIES) {
            var fileName = directory + clazz.getName();
            try {
                Enumeration<URL> urls = Objects.nonNull(this.classLoader) ? classLoader.getResources(fileName) : ClassLoader.getSystemResources(fileName);
                if (Objects.nonNull(urls)) {
                    while (urls.hasMoreElements()) {
                        var url = urls.nextElement();
                        loadResource(classes, url);
                    }
                }
            } catch (IOException e) {
                logger.error("load extension class error {}", fileName, e);
            }
        }
    }

    private void loadResource(HashMap<String, Class<?>> classes, URL url) {
        try (var in = url.openStream()) {
            var properties = new Properties();
            properties.load(in);
            properties.forEach((key, value) -> {
                var name = (String) key;
                var classPath = (String)value;
                if (!StringUtils.isEmpty(name) && !StringUtils.isEmpty(classPath)) {
                    try {
                        loadClass(classes, name, classPath);
                    } catch (ClassNotFoundException e) {
                        throw new IllegalStateException("load extensions resource error", e);
                    }
                }
            });
        } catch (Exception e) {
            throw new IllegalStateException("load extensions resource error ", e);
        }
    }

    private void loadClass(HashMap<String, Class<?>> classes, String name, String classPath) throws ClassNotFoundException {
        var subClass = Objects.nonNull(this.classLoader) ? Class.forName(classPath, true, this.classLoader) : Class.forName(classPath);
        if (!clazz.isAssignableFrom(subClass)) {
            throw new IllegalStateException("load extension resource error, "+ subClass+" subType is not of"+ clazz);
        }
        if (!subClass.isAnnotationPresent(SPIClass.class)) {
            throw new IllegalStateException("load extension resource error "+ subClass+" without @"+SPIClass.class+" annotation");
        }
        var oldClass = classes.get(name);
        if (Objects.isNull(oldClass)) {
            classes.put(name, subClass);
        }else if (!Objects.equals(oldClass, subClass)) {
            throw new IllegalStateException("load extension resource error, Duplicate class "+ clazz.getName()+" name"+ name +" on "+ oldClass.getName()+" or "+ subClass.getName());
        }
    }
}
